/*
本例子实现了gin框架下的多个大文件流式上传，避免了文件内容存在内存而无法支持大文件的情况
*/
package main

import (
	"bytes"
	"errors"
	"fmt"
	"github.com/gin-gonic/gin"
	"io"
	"log"
	"net/http"
	"os"
	"strconv"
	"strings"
)

const (
	ContentDisposition = "Content-Disposition: "
	NAME               = "name=\""
	FILENAME           = "filename=\""
	ContentType        = "Content-Type: "
	ContentLength      = "Content-Length: "
	BOUNDARY           = "; boundary="
)

/*
每一个分段必须以--boundary开始（前面加两个"-")
之后要用Content-Type和ContentRange标记这段数据的类型和所在范围
然后就像普通的响应头一样以回车换行结束
再后面是分段数据
最后用一个--boundary--（前后各有两个-）表示所有的分段结束
*/
// http://localhost:8080/upload.html
/*
cd s7-net/gin/upload/stream_upload
go run gin_stream_upload_file.go
*/
func main() {
	log.SetFlags(log.LstdFlags | log.Lshortfile)
	r := gin.Default()
	r.StaticFile("/upload.html", "./upload.html")

	r.POST("/gin_upload", uploadFile)

	log.Fatal(r.Run(":8080"))
}

// / 解析多个文件上传中，每个具体的文件的信息
type FileHeader struct {
	ContentDisposition string // 浏览器对文件的行为 下载还是打开 inline attachment
	Name               string
	FileName           string // 文件名
	ContentType        string // 文件类型
	ContentLength      int64  // 文件大小
}

func uploadFile(c *gin.Context) {
	// ----WebKitFormBoundaryUL8bPubZEKCwCk91
	boundary, err := getBoundary(c.Request)
	if err != nil {
		log.Println(err.Error())
		c.AbortWithError(http.StatusBadRequest, err)
		return
	}

	readDataBuf := make([]byte, 1024*1024)
	var readTotal = 0

	for {
		// 分段开头 ----WebKitFormBoundaryUL8bPubZEKCwCk91\r\n
		boundaryStart := append(boundary, []byte("\r\n")...)
		fileHeader, fileData, err := parseFromHead(readDataBuf, readTotal, boundaryStart, c.Request.Body)
		if err != nil {
			log.Printf("%v", err)
			return
		}
		log.Printf("file :%s\n", fileHeader.FileName)

		f, err := os.Create(fileHeader.FileName)
		if err != nil {
			log.Printf("create file fail:%v\n", err)
			return
		}
		f.Write(fileData)
		fileData = nil

		//需要反复搜索boundary
		tempData, reachEnd, err := readToBoundary(boundary, c.Request.Body, f)
		f.Close()
		if err != nil {
			log.Printf("%v\n", err)
			return
		}
		if reachEnd {
			break
		} else {
			copy(readDataBuf[0:], tempData)
			readTotal = len(tempData)
			continue
		}
	}
	// 上传成功
	c.JSON(200, gin.H{
		"message": fmt.Sprintf("%s", "ok"),
	})
}

// / 解析表单的头部
// / @param read_data 已经从流中读到的数据
// / @param read_total 已经从流中读到的数据长度
// / @param boundary 表单的分割字符串
// / @param stream 输入流
// / @return FileHeader 文件名等信息头
// / []byte 已经从流中读到的部分
// / error 是否发生错误
func parseFromHead(readDataBuf []byte, readTotal int, boundaryStart []byte, stream io.ReadCloser) (FileHeader, []byte, error) {
	buf := make([]byte, 1024*1024)
	foundBoundary := false
	boundaryLoc := -1
	var fileHeader FileHeader
	for {
		readLen, err := stream.Read(buf)
		if err != nil {
			if err != io.EOF {
				return fileHeader, nil, err
			}
			break
		}
		if readTotal+readLen > cap(readDataBuf) {
			return fileHeader, nil, fmt.Errorf("not found boundaryStart")
		}
		copy(readDataBuf[readTotal:], buf[:readLen])
		readTotal += readLen
		// 没到边界 循环继续
		if !foundBoundary {
			// ----WebKitFormBoundaryUL8bPubZEKCwCk91\r\n
			// 整个请求体末尾以 boundaryStart 加上两个横杠--的方式结尾
			boundaryLoc = bytes.Index(readDataBuf[:readTotal], boundaryStart)
			if -1 == boundaryLoc {
				continue
			}
			foundBoundary = true
		}

		startLoc := boundaryLoc + len(boundaryStart) // // ----WebKitFormBoundaryUL8bPubZEKCwCk91\r\n[xxxxxx\r\n\r\n]
		fileHeadLoc := bytes.Index(readDataBuf[startLoc:readTotal], []byte("\r\n\r\n"))
		if -1 == fileHeadLoc {
			continue
		}
		fileHeadLoc += startLoc
		ret := false
		fileHeader, ret = parseFileHeader(readDataBuf[startLoc:fileHeadLoc])
		if !ret {
			return fileHeader, nil, fmt.Errorf("parseFileHeader fail:%s", string(readDataBuf[startLoc:fileHeadLoc]))
		}
		return fileHeader, readDataBuf[fileHeadLoc+4 : readTotal], nil
	}
	return fileHeader, nil, fmt.Errorf("reach to sream EOF")
}

// / 解析描述文件信息的头部
// / @return FileHeader 文件名等信息的结构体
// / @return bool 解析成功还是失败
func parseFileHeader(h []byte) (FileHeader, bool) {
	arr := bytes.Split(h, []byte("\r\n"))
	var outHeader FileHeader
	outHeader.ContentLength = -1

	for _, item := range arr {
		if bytes.HasPrefix(item, []byte(ContentDisposition)) {
			l := len(ContentDisposition)
			arr1 := bytes.Split(item[l:], []byte("; "))
			outHeader.ContentDisposition = string(arr1[0])
			if bytes.HasPrefix(arr1[1], []byte(NAME)) {
				outHeader.Name = string(arr1[1][len(NAME) : len(arr1[1])-1])
			}
			l = len(arr1[2])
			if bytes.HasPrefix(arr1[2], []byte(FILENAME)) && arr1[2][l-1] == 0x22 {
				outHeader.FileName = string(arr1[2][len(FILENAME) : l-1])
			}
		} else if bytes.HasPrefix(item, []byte(ContentType)) {
			l := len(ContentType)
			outHeader.ContentType = string(item[l:])
		} else if bytes.HasPrefix(item, []byte(ContentLength)) {
			l := len(ContentLength)
			s := string(item[l:])
			content_length, err := strconv.ParseInt(s, 10, 64)
			if err != nil {
				log.Printf("content length error:%s", string(item))
				return outHeader, false
			} else {
				outHeader.ContentLength = content_length
			}
		} else {
			log.Printf("unknown:%s\n", string(item))
		}
	}
	if len(outHeader.FileName) == 0 {
		return outHeader, false
	}
	return outHeader, true
}

// / 从流中一直读到文件的末位
// / @return []byte 没有写到文件且又属于下一个文件的数据
// / @return bool 是否已经读到流的末位了
// / @return error 是否发生错误
func readToBoundary(boundary []byte, stream io.ReadCloser, target io.WriteCloser) ([]byte, bool, error) {
	readData := make([]byte, 1024*8)
	readDataLen := 0
	buf := make([]byte, 1024*4)
	bLen := len(boundary)
	reachEnd := false
	for !reachEnd {
		readLen, err := stream.Read(buf)
		if err != nil {
			if err != io.EOF && readLen <= 0 {
				return nil, true, err
			}
			reachEnd = true
		}
		//todo: 下面这一句很蠢，值得优化
		copy(readData[readDataLen:], buf[:readLen]) //追加到另一块buffer，仅仅只是为了搜索方便
		readDataLen += readLen
		if readDataLen < bLen+4 {
			continue
		}
		loc := bytes.Index(readData[:readDataLen], boundary)
		if loc >= 0 {
			//找到了结束位置
			target.Write(readData[:loc-4])
			return readData[loc:readDataLen], reachEnd, nil
		}

		target.Write(readData[:readDataLen-bLen-4])
		copy(readData[0:], readData[readDataLen-bLen-4:])
		readDataLen = bLen + 4
	}
	target.Write(readData[:readDataLen])
	return nil, reachEnd, nil
}

// 获取请求头中的Boundary
func getBoundary(request *http.Request) ([]byte, error) {
	/*
		Content-Length: 1981258352
		Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryUL8bPubZEKCwCk91
	*/
	//content_length := c.Request.ContentLength
	contentTypes, hasKey := request.Header["Content-Type"]
	if !hasKey || len(contentTypes) != 1 {
		log.Printf("Content-Type count error\n")
		return nil, errors.New("boundary not found")
	}
	contentType := contentTypes[0]
	// 文件分割
	boundaryLoc := strings.Index(contentType, BOUNDARY) // ; boundary=起始位置
	if -1 == boundaryLoc {
		log.Printf("Content-Type error, no boundary\n")
		return nil, errors.New("boundary not found")
	}
	boundary := []byte(contentType[(boundaryLoc + len(BOUNDARY)):]) // 值 ----WebKitFormBoundaryUL8bPubZEKCwCk91
	return boundary, nil
}
